﻿using System;
using System.Linq;
using System.Runtime.Serialization;
using Microsoft.Isam.Esent.Interop;

namespace Microsoft.Isam.Esent.Serialization.Attributes
{
	/// <summary>Date-time column type.</summary>
	/// <remarks>
	/// <para>Must be applied to a field/property of type 'DateTime' or 'DateTime?'.</para>
	/// <para>Only UTC values are allowed.</para>
	/// <para>The underlying ESENT column type is JET_coltypDateTime with JET_bitColumnFixed flag.
	/// It means the value is stored in VARIANT format, 8-byte floating point number that represents a date in fractional days since the year 1900.</para>
	/// <para>Consider using [EseFileTime] instead, which is way more precise.</para>
	/// </remarks>
	[Obsolete( "Please use [EseFileTime] instead.", true )]
	public class EseDateTimeAttribute : OptionallyNullableColumn
	{
		/// <summary>Initialize with the default column name, which is the name of the field/property.</summary>
		public EseDateTimeAttribute() { }
		/// <summary>Initialize with non-default column name.</summary>
		public EseDateTimeAttribute( string _columnName ) : base( _columnName ) { }

		/// <summary>Get column definition.</summary>
		public override JET_COLUMNDEF getColumnDef()
		{
			JET_COLUMNDEF res = base.getColumnDef();
			res.coltyp = JET_coltyp.DateTime;
			return res;
		}

		/// <summary>Throw an exception if this attribute is not compatible with the type of the field/property.</summary>
		/// <param name="t">The type of the field/property this attribute is applied.</param>
		public override void verifyTypeSupport( Type t )
		{
			verifyBasicTypeSupport<DateTime>( t );
		}

		/// <summary>Store the column value in the database.</summary>
		public override void Serialize( EseCursorBase cur, JET_COLUMNID idColumn, object value, bool bNewRecord )
		{
			if( serializeNull( cur, idColumn, value ) ) return;
			DateTime dt = (DateTime)( value );
			if( dt.Kind == DateTimeKind.Local )
				throw new ArgumentException( "Only UTC date and time can be handled by ESE serializer" );
			Api.SetColumn( cur.idSession, cur.idTable, idColumn, dt );
		}

		/// <summary>Retrieve the column value from the DB.</summary>
		public override object Deserialize( EseCursorBase cur, JET_COLUMNID idColumn )
		{
			DateTime? res = Api.RetrieveColumnAsDateTime( cur.idSession, cur.idTable, idColumn );
			if( null == res )
			{
				if( bFieldNullable ) return null;
				throw new SerializationException( "The column is marked 'ColumnNotNULL', however the NULL value was pulled from the database." );
			}
			return DateTime.SpecifyKind( (DateTime)res, DateTimeKind.Utc );
		}

		/// <summary>Make the search key for this column.</summary>
		public override void MakeKey( EseCursorBase cur, object val, MakeKeyGrbit flags )
		{
			if( val == null )
				Api.MakeKey( cur.idSession, cur.idTable, null, flags );
			else if( val is DateTime )
				Api.MakeKey( cur.idSession, cur.idTable, (DateTime)val, flags );
			else
				makeKeyException( val );
		}
	}

	/// <summary>The column to hold <see cref="DateTimeOffset" /> structure.</summary>
	/// <remarks>
	/// <para>Must be applied to a field/property of type 'DateTimeOffset' or 'DateTimeOffset?'.</para>
	/// <para>The underlying ESENT column type is JET_coltypBinary, fixed, 16 bytes long.</para>
	/// <para><see cref="DateTimeOffset" /> values are stored as 16 bytes fixed binary value.
	/// First 8 bytes are <see cref="DateTime.ToBinary" /> value of the <see cref="DateTimeOffset.DateTime" /> field,
	/// the last 8 bytes are <see cref="TimeSpan.Ticks" /> of the <see cref="DateTimeOffset.Offset" /> field.<br />
	/// Indexing over such columns is not supported.</para>
	/// </remarks>
	public class EseDateTimeOffsetAttribute : OptionallyNullableColumn
	{
		/// <summary>Initialize with the default column name, which is the name of the field/property.</summary>
		public EseDateTimeOffsetAttribute() { }
		/// <summary>Initialize with non-default column name.</summary>
		public EseDateTimeOffsetAttribute( string _columnName ) : base( _columnName ) { }

		/// <summary>Get column definition.</summary>
		public override JET_COLUMNDEF getColumnDef()
		{
			JET_COLUMNDEF res = base.getColumnDef();
			res.coltyp = JET_coltyp.Binary;
			res.cbMax = 16;
			return res;
		}

		/// <summary>Throw an exception if this attribute is not compatible with the type of the field/property.</summary>
		/// <param name="t">The type of the field/property this attribute is applied.</param>
		public override void verifyTypeSupport( Type t )
		{
			verifyBasicTypeSupport<DateTimeOffset>( t );
		}

		/// <summary>Store the DateTimeOffset value in the database, as a pair of 64-bit values.</summary>
		public override void Serialize( EseCursorBase cur, JET_COLUMNID idColumn, object value, bool bNewRecord )
		{
			if( serializeNull( cur, idColumn, value ) ) return;
			DateTimeOffset dt = (DateTimeOffset)( value );

			long lngLocal = dt.DateTime.ToBinary();
			long lngOffset = dt.Offset.Ticks;
			var byteArray = BitConverter
				.GetBytes( lngLocal )
				.Concat( BitConverter.GetBytes( lngOffset ) )
				.ToArray();

			Api.SetColumn( cur.idSession, cur.idTable, idColumn, byteArray );
		}

		/// <summary>Retrieve the column value from the DB.</summary>
		public override object Deserialize( EseCursorBase cur, JET_COLUMNID idColumn )
		{
			byte[] res = Api.RetrieveColumn( cur.idSession, cur.idTable, idColumn );
			if( null == res || 0 == res.Length )
			{
				if( bFieldNullable ) return null;
				throw new SerializationException( "The column is marked 'ColumnNotNULL', however the NULL value was pulled from the database." );
			}
			if( 16 != res.Length )
				throw new SerializationException( "The column of type DateTimeOffset must contain exactly 16 bytes." );

			long lngLocal = BitConverter.ToInt64( res, 0 );
			long lngOffset = BitConverter.ToInt64( res, 8 );
			DateTime dt = DateTime.FromBinary( lngLocal );
			TimeSpan ts = TimeSpan.FromTicks( lngOffset );
			return new DateTimeOffset( dt, ts );
		}
	}

	/// <summary>The column to hold <see cref="TimeZoneInfo" /> class.</summary>
	/// <remarks>
	/// <para>Must be applied to a field/property of type 'TimeZoneInfo'.</para>
	/// <para>The underlying ESENT column type is JET_coltypText, ASCII, up to 32 bytes long.
	/// The column stores the time zone identifier, which is a name of the registry key
	/// in "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" key.
	/// That's why this column can only hold a time zone listed in the registry.</para>
	/// </remarks>
	public class EseTimeZoneAttribute : EseColumnAttrubuteBase
	{
		/// <summary>Initialize with the default column name, which is the name of the field/property.</summary>
		public EseTimeZoneAttribute() { }
		/// <summary>Initialize with non-default column name.</summary>
		public EseTimeZoneAttribute( string _columnName ) : base( _columnName ) { }

		/// <summary>Always returns true.</summary>
		public override bool bFieldNullable { get { return true; } }

		/// <summary>Get column definition.</summary>
		public override JET_COLUMNDEF getColumnDef()
		{
			JET_COLUMNDEF res = new JET_COLUMNDEF();
			res.coltyp = JET_coltyp.Text;
			res.cp = JET_CP.ASCII;
			res.grbit = ColumndefGrbit.None;
			// The documentation on TimeZoneInfo.Id  says:
			// you should not assign it an identifier longer than 32 characters.
			res.cbMax = 32;
			return res;
		}

		/// <summary>Throw an exception if this attribute is not compatible with the type of the field/property.</summary>
		/// <param name="t">The type of the field/property this attribute is applied.</param>
		public override void verifyTypeSupport( Type t )
		{
			if( t.Equals( typeof( TimeZoneInfo ) ) ) return;
			throw new SerializationException();
		}

		/// <summary>Store the TimeZoneInfo.id value in the database.</summary>
		public override void Serialize( EseCursorBase cur, JET_COLUMNID idColumn, object value, bool bNewRecord )
		{
			if( serializeNull( cur, idColumn, value ) ) return;
			TimeZoneInfo tzi = value as TimeZoneInfo;
			if( null == tzi )
				throw new SerializationException( "The value is of wrong type, must be TimeZoneInfo" );
			Api.SetColumn( cur.idSession, cur.idTable, idColumn, tzi.Id, System.Text.Encoding.ASCII );
		}

		/// <summary>Retrieve the value from the DB,
		/// and call <see cref="TimeZoneInfo.FindSystemTimeZoneById" /> to get the time zone.</summary>
		public override object Deserialize( EseCursorBase cur, JET_COLUMNID idColumn )
		{
			string id = Api.RetrieveColumnAsString( cur.idSession, cur.idTable, idColumn, System.Text.Encoding.ASCII );
			if( string.IsNullOrEmpty( id ) )
				return null;
			return TimeZoneInfo.FindSystemTimeZoneById( id );
		}
	}
}